# 路由插件注册
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
源码如下:
function install (Vue) {
if (install.installed && _Vue === Vue) { return }
install.installed = true;
_Vue = Vue;
var isDef = function (v) { return v !== undefined; };
var registerInstance = function (vm, callVal) {
var i = vm.$options._parentVnode;
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal);
}
};
Vue.mixin({
beforeCreate: function beforeCreate() {
// 注册路由实例
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
// 对 _route 进行相应式处理
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
// 销毁路由实例
registerInstance(this);
}
});
// 向 vue 原型上挂载 $router 和 $route
Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
// 注册全局组件 RouterView 和 RouterLink
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
var strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}
首先,是判断逻辑,确保插件只注册一次。
然后,通过 Vue.mixin
注入 vue 生命周期钩子beforeCreate
和destroyed
。
beforeCreate
:设置根路由;注册路由实例。destroyed
:销毁路由实例。
接着,向 vue 原型上挂载 $router
和$route
。
接着,注册全局路由组件RouterLink
和RouterView
。
最后,对相关钩子进行合并 (opens new window)。
# 实例化路由对象
export default new VueRouter({
routes: [
name: 'Home',
path: '/',
component: () => import('../view/home.vue')
]
})
主要完成以下逻辑:
- 处理参数;
- 创建路由匹配器 matcher;
- 根据不同的路由模式 mode 实例化对应的路由模式对象。
var VueRouter = function VueRouter(options) {
if ( options === void 0 ) options = {};
if (process.env.NODE_ENV !== 'production') {
warn(this instanceof VueRouter, "Router must be called with the new operator.");
}
// 参数处理
this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
this.matcher = createMatcher(options.routes || [], this); // 创建路由匹配器 matcher
var mode = options.mode || 'hash';
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = 'hash';
}
if (!inBrowser) {
mode = 'abstract';
}
this.mode = mode;
// 根据不同的路由模式 mode 实例化对应的路由模式对象。
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback);
break
case 'abstract':
this.history = new AbstractHistory(this, options.base);
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, ("invalid mode: " + mode));
}
}
};
# 创建路由匹配器 matcher
该函数传递参数路由表和当前路由对象,主要完成:
- 创建路由映射表;
- 返回包含 match、addRoute、getRoutes、addRoutes 四个函数的路由匹配器对象。
function createMatcher (
routes,
router
) {
var ref = createRouteMap(routes); // 创建路由映射表
var pathList = ref.pathList;
var pathMap = ref.pathMap;
var nameMap = ref.nameMap;
// ... 一些函数定义
function addRoutes(routes) {
// ...
}
function addRoute (parentOrRoute, route) {
// ...
}
function getRoutes () {
return pathList.map(function (path) { return pathMap[path]; })
}
function match (
raw,
currentRoute,
redirectedFrom
) {
// ...
}
function redirect (
record,
location
) {
// ...
}
function alias (
record,
location,
matchAs
) {
// ...
}
function _createRoute (
record,
location,
redirectedFrom
) {
// ...
}
// 返回包含 match、addRoute、getRoutes、addRoutes 四个函数的路由匹配器对象
return {
match: match,
addRoute: addRoute,
getRoutes: getRoutes,
addRoutes: addRoutes
}
}
# 创建路由映射表
该函数 createRouteMap 主要建立命名路由和路由路径的映射表。
如路由表为:
new VueRouter({
routes: [
{
name: 'Home',
path: getRoute(process.env.LANG).home,
component: () => import('../views/home.vue'),
children: [
{
path: 'subHome',
name: 'SubHome',
component: () => import('../views/subHome.vue')
}
]
}
]
})
生成的路由映射关系为:
ref = {
"pathList": [
"/home/subHome",
"/home"
],
"pathMap": {
"/home/subHome": {
"path": "/home/subHome",
"regex": {
"keys": []
},
"components": {}, // 对应路由组件
"alias": [],
"instances": {},
"enteredCbs": {},
"name": "SubHome",
"parent": {
"path": "/home",
"regex": {
"keys": []
},
"components": {},
"alias": [],
"instances": {},
"enteredCbs": {},
"name": "Home",
"meta": {},
"props": {}
},
"meta": {},
"props": {}
},
"/home": {
"path": "/home",
"regex": {
"keys": []
},
"components": {},
"alias": [],
"instances": {},
"enteredCbs": {},
"name": "Home",
"meta": {},
"props": {}
}
},
"nameMap": {
"SubHome": {
"path": "/home/subHome",
"regex": {
"keys": []
},
"components": {},
"alias": [],
"instances": {},
"enteredCbs": {},
"name": "SubHome",
"parent": {
"path": "/home",
"regex": {
"keys": []
},
"components": {},
"alias": [],
"instances": {},
"enteredCbs": {},
"name": "Home",
"meta": {},
"props": {}
},
"meta": {},
"props": {}
},
"Home": {
"path": "/home",
"regex": {
"keys": []
},
"components": {},
"alias": [],
"instances": {},
"enteredCbs": {},
"name": "Home",
"meta": {},
"props": {}
}
}
}
可见,即使是嵌套路由,都会扁平化为一级。每个 key(路由路径和路由名)都包含以下基本信息:
{
"path": "",
"regex": {
"keys": []
},
"components": {},
"alias": [],
"instances": {},
"enteredCbs": {},
"name": "Home",
"meta": {},
"props": {},
"parent": {}
}
createRouteMap 函数内部逻辑为:
- 遍历路由表 routes,对每个路由项调用 addRouteRecord 函数进行处理,返回上面的基本信息;
- 如果当前路由项包含 children(子路由),则遍历子路由进行递归调用 addRouteRecord 处理;
- 如果当前路由包含 alias 别名,则把该别名当作新路由名,递归调用 addRouteRecord 处理。
createRouteMap 主要源码逻辑:
function createRouteMap (
routes,
oldPathList,
oldPathMap,
oldNameMap,
parentRoute
) {
// ...
var pathList = oldPathList || [];
var pathMap = oldPathMap || Object.create(null);
var nameMap = oldNameMap || Object.create(null);
routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute);
});
// 通配符 * 处理,确保在路由表的最后
for (var i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0]);
l--;
i--;
}
}
// ...
return {
pathList: pathList,
pathMap: pathMap,
nameMap: nameMap
}
}
function addRouteRecord (
pathList,
pathMap,
nameMap,
route,
parent,
matchAs
) {
var path = route.path;
var name = route.name;
var pathToRegexpOptions =
route.pathToRegexpOptions || {};
var normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict); // 规范化路径
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive;
}
// 映射表基本信息
var record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
alias: route.alias
? typeof route.alias === 'string'
? [route.alias]
: route.alias
: [],
instances: {},
enteredCbs: {},
name: name,
parent: parent,
matchAs: matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
};
if (route.children) {
// ...
// 遍历子路由列表,递归处理子路由
route.children.forEach(function (child) {
var childMatchAs = matchAs
? cleanPath((matchAs + "/" + (child.path)))
: undefined;
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
});
}
// 添加到路由映射表
if (!pathMap[record.path]) {
pathList.push(record.path);
pathMap[record.path] = record;
}
// 路由别名处理,相当于一个新路由名,也是递归调用 addRouteRecord
if (route.alias !== undefined) {
var aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
for (var i = 0; i < aliases.length; ++i) {
var alias = aliases[i];
// ...
var aliasRoute = {
path: alias,
children: route.children
};
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
);
}
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record;
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
"Duplicate named routes definition: " +
"{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
);
}
}
}
# 添加到 vue 实例
在 main.js 中添加 router 实例,并创建 vue 对象。
new Vue({
router,
// ...
})
当实例化 Vue 对象的时候,之前通过插件机制注入的 mixin 逻辑就会执行。
function install (Vue) {
// ...
Vue.mixin({
beforeCreate: function beforeCreate() {
if (isDef(this.$options.router)) {
this._routerRoot = this; //
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
registerInstance(this);
}
});
// ...
}
这里主要执行:
this._routerRoot = this; // _routerRoot 指向当前 Vue 实例对象
this._router = this.$options.router; // _router 指向 VueRouter 实例对象
this._router.init(this); // 执行初始化逻辑
Vue.util.defineReactive(this, '_route', this._router.history.current); // 将 _route 设置为响应式
- 将 _routerRoot 指向当前 Vue 实例对象;
- 将 _router 指向 VueRouter 实例对象;
- 调用路由初始化函数;
- 将 _route 设置为响应式。
# 路由初始化 init 函数
该函数定义在 VueRouter 原型上,主要源码如下:
VueRouter.prototype.init = function init(app /* Vue component instance */) {
var this$1 = this;
// ...
this.apps.push(app);
// ...
// 如果 vue 实例已经被初始化过了,直接返回。因为路由监听器只需要一个
if (this.app) {
return
}
this.app = app;
var history = this.history; // 获取当前路由模式对象
if (history instanceof HTML5History || history instanceof HashHistory) {
var handleInitialScroll = function (routeOrError) {
var from = history.current;
var expectScroll = this$1.options.scrollBehavior;
var supportsScroll = supportsPushState && expectScroll;
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this$1, routeOrError, from, false);
}
};
var setupListeners = function (routeOrError) {
history.setupListeners();
handleInitialScroll(routeOrError);
};
// 调用 transitionTo,获取当前浏览器 location,更新对应路由信息
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
);
}
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
};
init 函数主要逻辑:获取当前 vue 实例,并确保初始化逻辑只执行一次(因为路由监听器只需要一个);然后获取对应的路由模式对象,调用 transitionTo 函数,获取当前浏览器 location,更新对应路由信息。
transitionTo 函数比较复杂,后面单独拎出来看源码。
# 渲染路由组件
执行代码逻辑为:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
路由组件,主要是调用组件<router-view />
进行渲染,该组件在 VueRouter 插件机制已完成全局组件注册。
function install (Vue) {
// ...
// 注册全局组件 RouterView
Vue.component('RouterView', View);
// ...
}
RouterView 组件主要逻辑为:
- 查到路由配置表,匹配到对应的路由信息;
- 通过 $createElement 渲染组件。
RouterView 组件源码:
var View = {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render: function render(_, ref) {
var props = ref.props;
var children = ref.children;
var parent = ref.parent;
var data = ref.data;
// 设置是 routerView 组件标志
data.routerView = true;
// 直接使用父组件的 createElement() 方法,这样 router-view 渲染的组件就可以解析命名槽
var h = parent.$createElement;
var name = props.name;
var route = parent.$route;
var cache = parent._routerViewCache || (parent._routerViewCache = {});
var depth = 0;
var inactive = false;
// 循环查找父实例,获取注册 router 实例的根 vue 实例,以及当前 router-view 组件的层级 depth,通过该层级 depth 可以在 route.matched 匹配到对应的路由组件
while (parent && parent._routerRoot !== parent) {
var vnodeData = parent.$vnode ? parent.$vnode.data : {};
if (vnodeData.routerView) {
depth++;
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true;
}
parent = parent.$parent;
}
data.routerViewDepth = depth;
// keep-alive 处理
if (inactive) {
var cachedData = cache[name];
var cachedComponent = cachedData && cachedData.component;
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps);
}
return h(cachedComponent, data, children)
} else {
// render previous empty view
return h()
}
}
// 获取对应的路由组件
var matched = route.matched[depth];
var component = matched && matched.components[name];
// 空路由组件判断处理
if (!matched || !component) {
cache[name] = null;
return h()
}
// 缓存匹配到的路由组件
cache[name] = { component: component };
// 在 data 对象上定义 registerRouteInstance 注册路由实例方法,该方法通过插件机制注入 mixin 中的 beforeCreate 钩子执行
data.registerRouteInstance = function (vm, val) {
var current = matched.instances[name];
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val;
}
}
// ...
// 渲染组件
return h(component, data, children)
}
};
以上代码主要执行:
- 设置是 router-view 组件标志(用于后面获取当前 router-view 组件层级 depth);
- 获取 router-view 组件的父组件及对应的 $createElement 方法。通过父组件的 $createElement 方法渲染路由组件, 是为了 router-view 渲染的组件就可以解析命名槽;
- 循环查找父实例,获取注册 router 实例的根 vue 实例,以及当前 router-view 组件的层级 depth,通过该层级 depth 可以在 route.matched 匹配到当前 location 对应的路由组件;
- 获取对应的路由组件;
- 空路由组件判断处理;
- 缓存匹配到的路由组件;
- 在 data 对象上定义 registerRouteInstance 注册路由实例方法,该方法通过插件机制注入 mixin 中的 beforeCreate 钩子执行;
- 通过父组件的 $createElement 方法渲染路由组件。
上面的倒数第二点,函数调用逻辑如下:
var registerInstance = function (vm, callVal) {
var i = vm.$options._parentVnode;
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal);
}
};
Vue.mixin({
beforeCreate: function beforeCreate() {
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
registerInstance(this);
}
});
在 router-view 组件的 render 函数添加打印,输出 parent :console.log('>> router-view render parent:', parent);
在 mixin 的 registerInstance 中打印信息:console.log('>> vue mixin beforeCreate registerInstance:', vm, callVal);
得到打印输出如下:
>> router-view render parent: VueComponent {_uid: 1, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
>> vue mixin beforeCreate registerInstance: VueComponent {_uid: 2, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
VueComponent {_uid: 2, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
在实际的调试中,发现每次打印的最开始都会多一次输出:
>> router-view render parent: VueComponent {_uid: 1, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
在 router-view 组件的 render 函数添加打印,输出 routes:console.log('>> router-view render route:', route);
,得到输出:
ViewRouter render route: {name: null, meta: {…}, path: '/', hash: '', query: {…}, …}
/
根路径在路由表中并没有配置,但是每次都输出了。通过 debugger 发现,在源码中,会有一个 router-view 的调用逻辑。源码如下:
function createRoute (
record,
location,
redirectedFrom,
router
) {
var stringifyQuery = router && router.options.stringifyQuery;
var query = location.query || {};
try {
query = clone(query);
} catch (e) {}
var route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
};
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery);
}
return Object.freeze(route)
}
// the starting route that represents the initial state
var START = createRoute(null, {
path: '/'
});
流程为:
- 当 init 路由初始化函数(将 VueRouter 添加到 vue 实例阶段)还没获取当前 location 之前,router-view 执行一次,使用初始路径
/
进行路由组件渲染,所以没有匹配到对应组件; - 当 init 路由初始化函数获取当前 location 并更新当前路由信息时,router-view 会再执行一次。